﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Common.Utilities;
using Workflows.Components.Entities.Addresses;

namespace Workflows.Components.Entities.Distances.FieldDistances
{
    public class AddressComparer:IAttributeDistanceCalcutor
    {
        private Dictionary<string,GeoCode> _ZipCodeGeoCodes;
        /// <summary></summary>
        public Dictionary<string, GeoCode> ZipCodeGeoCodes
        {
            get
            {
                return this._ZipCodeGeoCodes;
            }
            set
            {
                this._ZipCodeGeoCodes = value;
            }
        }

        private void LoadZipGeoCodes()
        {
            this._ZipCodeGeoCodes = new Dictionary<string, GeoCode>();
            string baseResourceName = "Workflows.Components.Entities.Resources";
            StreamReader streamReader =
                ResourceUtil.GetEmbeddedFileContent(typeof (AddressComparer).Assembly,
                                                    baseResourceName + ".zcta5.txt");
            if(streamReader !=null)
            {
                while(!streamReader.EndOfStream)
                {
                    string line = streamReader.ReadLine();
                    string zipCode = line.Substring(2, 5);
                    string latitudeStr = line.Substring(136, 10);
                    string longitudeStr = line.Substring(146, 11);
                    GeoCode geoCode = new GeoCode();
                    geoCode.Latitude = double.Parse(latitudeStr);
                    geoCode.Longitude = double.Parse(longitudeStr);
                    if(!this._ZipCodeGeoCodes.ContainsKey(zipCode))
                    {
                        this._ZipCodeGeoCodes.Add(zipCode, geoCode);
                    }
                }
                streamReader.Close();
            }
        }

        #region IAttributeComparer Members
        /// <summary>
        /// load dictionary into memory or read config settings from file
        /// </summary>
        public void InitInMemorySettings()
        {
            this.LoadZipGeoCodes();
        }

        /// <summary>
        /// extract zip code (US address only) and returns geo-distance in miles
        /// similarity is normalized to 100 miles, i.e. any two places within 100 miles have 1.0 similarity score
        /// Math.Min(1,100/Math.Max(1,distance))
        /// </summary>
        /// <param name="attrValue1"></param>
        /// <param name="attrValue2"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public double Compare(object attrValue1, object attrValue2, ref MatchResult result)
        {
            result = MatchResult.NullMatch;
            if (attrValue1 == null || attrValue2 == null)
                return 0;

            string strVal1 = attrValue1.ToString();
            string strVal2 = attrValue2.ToString();
            if (strVal1.Length == 0 || strVal2.Length == 0)
                return 0;

            string pattern = "\\d{5}";
            if (Regex.IsMatch(strVal1, pattern) || Regex.IsMatch(strVal2, pattern))
            {
                string zip1 = "";
                Match match = Regex.Match(strVal1, pattern, RegexOptions.IgnoreCase);
                while(match.Success)
                {
                    zip1 = match.Value;
                    match = match.NextMatch();
                }
                string zip2 = "";
                match = Regex.Match(strVal2, pattern, RegexOptions.IgnoreCase);
                while(match.Success)
                {
                    zip2 = match.Value;
                    match = match.NextMatch();
                }
                if (this._ZipCodeGeoCodes.ContainsKey(zip1) && this._ZipCodeGeoCodes.ContainsKey(zip2))
                {
                    double distance = this.GetDistanceBetweenTwoGeoCodes(
                        this._ZipCodeGeoCodes[zip1], this._ZipCodeGeoCodes[zip2]);
                    result = MatchResult.PositiveMatch;
                    if (distance <= 100)
                        result = MatchResult.PerfectMatch;
                    return Math.Min(1, 100/Math.Max(1, distance));
                }
            }

            result = MatchResult.NegativeMatch;
            return 0;
        }

        public double Compare(Entity entity1, Entity entity2, AttributeMetaData attrMetaData, ref MatchResult result)
        {
            
            Debug.Assert(!string.IsNullOrEmpty(attrMetaData.AttributeName));

            string attrName = attrMetaData.AttributeName;
            object attrValue1 = null;
            object attrValue2 = null;
            if (entity1.ComparableAttributes.ContainsKey(attrName))
                attrValue1 = entity1.ComparableAttributes[attrName];
            if (entity2.ComparableAttributes.ContainsKey(attrName))
                attrValue2 = entity2.ComparableAttributes[attrName];
            return this.Compare(attrValue1, attrValue2, ref result);
        }

        public string[] ArgumentNames
        {
            get
            {
                return new string[] {"Address"};
            }
        }

        public Dictionary<string, string> ComparerSettings
        {
            get { return new Dictionary<string, string>(); }
            set {  }
        }

        private double GetDistanceBetweenTwoGeoCodes(GeoCode geoCode1, GeoCode geoCode2)
        {
            double earchRadius = 3956.087107103049;
            double latitude1Radians = (geoCode1.Latitude / 180) * Math.PI;
            double longitude1Radians = (geoCode1.Longitude / 180) * Math.PI;
            double latitude2Radians = (geoCode2.Latitude / 180) * Math.PI;
            double longitude2Radians = (geoCode2.Longitude / 180) * Math.PI;
            double distance =
                (earchRadius*2)*
                Math.Asin(
                    Math.Sqrt(
                        Math.Pow(Math.Sin((latitude1Radians - latitude2Radians)/2), 2) +
                        Math.Cos(latitude1Radians)*
                        Math.Cos(latitude2Radians)*
                        Math.Pow(Math.Sin((longitude1Radians - longitude2Radians)/2), 2)
                        )
                    );
            return distance;
        }
        #endregion
    }
}
